package gu.sql2java.observer;

import java.io.IOException;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Enumeration;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;

import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;

import gu.sql2java.RowMetaData;
import gu.sql2java.exception.RuntimeDaoException;
import gu.sql2java.velocity.Sql2javaClasspathResourceLoader;
import gu.sql2java.velocity.TemplateGenerator;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static gu.sql2java.velocity.TemplateGenerator.generateFile;
import static gu.sql2java.velocity.TemplateGenerator.generateString;

/**
 * 触发器SQL语句生成类(当前只支持MySQL),
 * 依赖 com.gitee.l0km:sql2java-velocity
 * @author guyadong
 *
 */
public class TriggerGenerator {
	/**
	 * 保存表对应的字段名列表
	 */
	private final ConcurrentHashMap<String, List<String>> columnNames = new ConcurrentHashMap<>();

	/**
	 * 模板路径的数据名前缀
	 */
	private String dbprefix = null;
	private DatabaseMetaData meta;
	private String catalog;

	public TriggerGenerator() {
		init();
	}

	public TriggerGenerator(Connection connection) {
		this();
		setConnection(connection);
	}

	public TriggerGenerator(DatabaseMetaData meta) {
		this();
		setDatabaseMetaData(checkNotNull(meta, "meta is null"));
	}

	/**
	 * velocity引擎初始化
	 */
	private void init() {
		Properties vprops = new Properties();
		vprops.put(Velocity.SET_NULL_ALLOWED, "true");
		vprops.put(Velocity.INPUT_ENCODING, "UTF-8");
		vprops.put(Velocity.OUTPUT_ENCODING, "UTF-8");
		vprops.put(Velocity.VM_LIBRARY, "macros.include.vm");
		vprops.put(Velocity.RESOURCE_LOADER, "class");
		vprops.put("class.resource.loader.description", "Velocity Classpath Resource Loader");
		vprops.put("class.resource.loader.class", Sql2javaClasspathResourceLoader.class.getName());
		vprops.put("class.resource.loader.prefix",
				"/templates/velocity/ro_notify/includes,/templates/velocity/ro_notify");
		TemplateGenerator.init(vprops);
	}

	/**
	 * 根据提供的参数生成Velocity引擎工作时的上下文对象{@link VelocityContext}
	 * 
	 * @param schematable
	 *            表名(schema+tablename)
	 * @param columnNames
	 *            表字段名列表
	 * @param properties
	 *            额外的模板变量定义,可为{@code null}
	 * @return {@link VelocityContext}
	 */
	private VelocityContext createConext(String schematable, List<String> columnNames, Properties properties) {
		String tablename = JDBCUtility.parseTablenme(schematable);
		VelocityContext context = new VelocityContext();
		context.put("schematable", schematable);
		context.put("tablename", tablename);
		context.put("columnNames", columnNames);
		if (null != properties) {
			for (@SuppressWarnings("unchecked")
			Enumeration<String> e = (Enumeration<String>) properties.propertyNames(); e.hasMoreElements();) {
				String key = e.nextElement();
				context.put(key, properties.get(key));
			}
		}
		return context;
	}

	/**
	 * 如果{@link #meta}字段不为null,则尝试通过JDBC 接口返回指定表的字段名列表
	 * 
	 * @param schematable 表名(schema+tablename)
	 * @return 字段名列表
	 */
	private List<String> onAbsentByJDBC(String schematable) {
		if (null != meta) {
			/** 从 schematable 解析出数据库名和表名 */
			Pattern p = Pattern.compile("^(\\w+)\\.(\\w+)$");
			Matcher m = p.matcher(checkNotNull(schematable, "schematable"));
			checkArgument(m.find(), "INVALID schematable format,${schema}.${tablename} required");
			String schema = m.group(1);
			String tablename = m.group(2);
			try {
				ImmutableList.Builder<String> builder = ImmutableList.builder();
				ResultSet resultSet = checkNotNull(meta, "meta is uninitialized").getColumns(catalog, schema, tablename,
						"%");
				while (resultSet.next()) {
					String column = resultSet.getString("COLUMN_NAME");
					builder.add(column);
					// System.out.println(tablename + "." + column + " found");
				}
				resultSet.close();
				return builder.build();
			} catch (SQLException e) {
				throw new RuntimeDaoException(e);
			}
		}
		return null;
	}

	/**
	 * 通过 {@link RowMetaData}获取表字段名列表
	 * 
	 * @param schematable 表名(schema+tablename)
	 * @return 字段名列表
	 */
	private List<String> onAbsentByMetadata(String schematable) {
		try {
			String tablename = JDBCUtility.parseTablenme(schematable);
			RowMetaData rowMetaData = RowMetaData.getMetaData(tablename);
			return rowMetaData.columnNames;
		} catch (Exception e) {
			return null;
		}
	}

	/**
	 * 当 {@link #columnNames}中没有指定表的字段名列表时，调用此方法 子类可以重写些方法,返回指定表的字段名列表
	 * 
	 * @param schematable 表名(schema+tablename)
	 * @return 字段名列表
	 */
	protected List<String> onAbsent(String schematable) {
		List<String> names = onAbsentByJDBC(schematable);
		if (null != names) {
			return names;
		}
		return onAbsentByMetadata(schematable);
	}

	/**
	 * 指定表的字段名列表
	 * 
	 * @param schematable 表名(schema+tablename)
	 * @param columnNames
	 * @return 当前对象
	 */
	public TriggerGenerator setColumnNames(String schematable, List<String> columnNames) {
		this.columnNames.put(checkNotNull(schematable, "schematable is null"),
				checkNotNull(columnNames, "columnNames is null"));
		return this;
	}

	private List<String> getColumnNames(String schematable) {
		List<String> names = columnNames.get(schematable);
		if (null == names) {
			synchronized (columnNames) {
				names = onAbsent(schematable);
				if (names != null) {
					columnNames.put(schematable, names);
				}
			}
		}
		return checkNotNull(columnNames.get(schematable), "FAIL TO get column names for %s", schematable);
	}

	/**
	 * 设置模板路径的数据名前缀,未设置时默认为'mysql', 当前只支持'mysql'
	 * 
	 * @param dbprefix
	 * @return 当前对象
	 */
	public TriggerGenerator setDbprefix(String dbprefix) {
		checkArgument(null == dbprefix || "mysql".equals(dbprefix),
				"UNSUPPORTE dbprefix %s,for dbprefix,only 'mysql' supported", dbprefix);
		this.dbprefix = dbprefix;
		return this;
	}

	/**
	 * @return 返回模板路径的数据名前缀,默认为'mysql'
	 */
	private String getDbprefix() {
		return MoreObjects.firstNonNull(dbprefix, "mysql");
	}

	/**
	 * 指定数据库连接，用于通过JDBC接口获取字段名列表
	 * 
	 * @param connection
	 * @return 当前对象
	 */
	public TriggerGenerator setConnection(Connection connection) {
		try {
			this.meta = checkNotNull(connection, "connection is null").getMetaData();
		} catch (SQLException e) {
			throw new RuntimeDaoException(e);
		}
		return this;
	}

	/**
	 * 指定{@link DatabaseMetaData}对象，用于通过JDBC接口获取字段名列表
	 * 
	 * @param meta
	 * @return 当前对象
	 */
	public TriggerGenerator setDatabaseMetaData(DatabaseMetaData meta) {
		this.meta = meta;
		return this;
	}

	/**
	 * 指定 catalog 参数，，用于通过JDBC接口获取字段名列表，默认为null
	 * 
	 * @param catalog
	 * @return 当前对象
	 * @see {@link DatabaseMetaData#getColumns(String, String, String, String)}
	 */
	public TriggerGenerator setCatalog(String catalog) {
		this.catalog = catalog;
		return this;
	}

	/**
	 * 生成指定表名的创建触发器SQL语句
	 * 
	 * @param schematable 表名(schema+tablename)
	 * @param properties
	 *            额外的模板变量定义,可为{@code null}
	 * @param install
	 *            为true生成安装脚本,否则生成删除脚本
	 */
	public String generateTriggerSQL(String schematable, Properties properties, boolean install) {
		// 删除脚本不需要字段名列表
		VelocityContext context = createConext(schematable, (install ? getColumnNames(schematable) : null), properties);
		return generateString(context, checkNotNull(getDbprefix(), "dbproduct is null")
				+ (install ? "/install_ro_notify_trigger.sql.vm" : "/remove_ro_notify_trigger.sql.vm"));
	}

	/**
	 * 生成指定表名的创建触发器SQL语句，写入到指定文件
	 * 
	 * @param schematable 表名(schema+tablename)
	 * @param filename
	 * @param properties
	 *            额外的模板变量定义,可为{@code null}
	 * @param install
	 *            为true生成安装脚本,否则生成删除脚本
	 * @throws IOException
	 */
	public void generateTriggerSQLFile(String schematable, String filename, Properties properties, boolean install)
			throws IOException {
		// 删除脚本不需要字段名列表
		VelocityContext context = createConext(schematable, (install ? getColumnNames(schematable) : null), properties);
		generateFile(context,
				checkNotNull(getDbprefix(), "dbproduct is null")
						+ (install ? "/install_ro_notify_trigger.sql.vm" : "/remove_ro_notify_trigger.sql.vm"),
				filename);
	}
}
